Buka pemrosesan video canggih berbasis browser. Pelajari cara mengakses dan memanipulasi data plane VideoFrame mentah secara langsung dengan WebCodecs API untuk efek dan analisis kustom.
Akses Plane VideoFrame WebCodecs: Penyelaman Mendalam ke Manipulasi Data Video Mentah
Selama bertahun-tahun, pemrosesan video berkinerja tinggi di browser web terasa seperti mimpi yang jauh. Pengembang sering kali terbatas pada limitasi elemen <video> dan API Canvas 2D, yang meskipun kuat, menimbulkan hambatan kinerja dan membatasi akses ke data video mentah yang mendasarinya. Kehadiran API WebCodecs telah mengubah lanskap ini secara fundamental, menyediakan akses tingkat rendah ke codec media bawaan browser. Salah satu fiturnya yang paling revolusioner adalah kemampuan untuk mengakses dan memanipulasi data mentah dari frame video individual secara langsung melalui objek VideoFrame.
Artikel ini adalah panduan komprehensif bagi para pengembang yang ingin melampaui pemutaran video sederhana. Kita akan menjelajahi seluk-beluk akses plane VideoFrame, mengupas konsep seperti ruang warna dan tata letak memori, serta memberikan contoh praktis untuk memberdayakan Anda membangun aplikasi video dalam browser generasi berikutnya, dari filter real-time hingga tugas visi komputer yang canggih.
Prasyarat
Untuk mendapatkan manfaat maksimal dari panduan ini, Anda harus memiliki pemahaman yang kuat tentang:
- JavaScript Modern: Termasuk pemrograman asinkron (
async/await, Promises). - Konsep Dasar Video: Keakraban dengan istilah seperti frame, resolusi, dan codec akan sangat membantu.
- API Browser: Pengalaman dengan API seperti Canvas 2D atau WebGL akan bermanfaat tetapi tidak mutlak diperlukan.
Memahami Frame Video, Ruang Warna, dan Plane
Sebelum kita mendalami API, kita harus terlebih dahulu membangun model mental yang kokoh tentang seperti apa sebenarnya data sebuah frame video. Video digital adalah urutan gambar diam, atau frame. Setiap frame adalah kisi-kisi piksel, dan setiap piksel memiliki warna. Bagaimana warna itu disimpan ditentukan oleh ruang warna dan format piksel.
RGBA: Bahasa Asli Web
Sebagian besar pengembang web akrab dengan model warna RGBA. Setiap piksel diwakili oleh empat komponen: Merah (Red), Hijau (Green), Biru (Blue), dan Alfa (transparansi). Data biasanya disimpan secara interleaved (berselang-seling) di memori, yang berarti nilai R, G, B, dan A untuk satu piksel disimpan secara berurutan:
[R1, G1, B1, A1, R2, G2, B2, A2, ...]
Dalam model ini, seluruh gambar disimpan dalam satu blok memori tunggal yang berkesinambungan. Kita dapat menganggap ini sebagai memiliki satu "plane" data.
YUV: Bahasa Kompresi Video
Namun, codec video jarang bekerja dengan RGBA secara langsung. Mereka lebih suka ruang warna YUV (atau lebih akuratnya, Y'CbCr). Model ini memisahkan informasi gambar menjadi:
- Y (Luma): Informasi kecerahan atau grayscale. Mata manusia paling sensitif terhadap perubahan luma.
- U (Cb) dan V (Cr): Informasi krominans atau perbedaan warna. Mata manusia kurang sensitif terhadap detail warna dibandingkan detail kecerahan.
Pemisahan ini adalah kunci kompresi yang efisien. Dengan mengurangi resolusi komponen U dan V—sebuah teknik yang disebut chroma subsampling—kita dapat secara signifikan mengurangi ukuran file dengan kehilangan kualitas yang minimal. Hal ini mengarah pada format piksel planar, di mana komponen Y, U, dan V disimpan dalam blok memori terpisah, atau "plane".
Format yang umum adalah I420 (sejenis YUV 4:2:0), di mana untuk setiap blok piksel 2x2, terdapat empat sampel Y tetapi hanya satu sampel U dan satu sampel V. Ini berarti plane U dan V memiliki setengah lebar dan setengah tinggi dari plane Y.
Memahami perbedaan ini sangat penting karena WebCodecs memberi Anda akses langsung ke plane-plane ini, persis seperti yang disediakan oleh decoder.
Objek VideoFrame: Gerbang Anda Menuju Data Piksel
Bagian utama dari teka-teki ini adalah objek VideoFrame. Objek ini mewakili satu frame video dan tidak hanya berisi data piksel tetapi juga metadata penting.
Properti Kunci dari VideoFrame
format: String yang menunjukkan format piksel (misalnya, 'I420', 'NV12', 'RGBA').codedWidth/codedHeight: Dimensi penuh dari frame seperti yang disimpan di memori, termasuk padding apa pun yang diperlukan oleh codec.displayWidth/displayHeight: Dimensi yang harus digunakan untuk menampilkan frame.timestamp: Timestamp presentasi frame dalam mikrodetik.duration: Durasi frame dalam mikrodetik.
Metode Ajaib: copyTo()
Metode utama untuk mengakses data piksel mentah adalah videoFrame.copyTo(destination, options). Metode asinkron ini menyalin data plane dari frame ke dalam buffer yang Anda sediakan.
destination: SebuahArrayBufferatau typed array (sepertiUint8Array) yang cukup besar untuk menampung data.options: Sebuah objek yang menentukan plane mana yang akan disalin dan tata letak memorinya. Jika dihilangkan, metode ini akan menyalin semua plane ke dalam satu buffer yang berkesinambungan.
Metode ini mengembalikan sebuah Promise yang resolve dengan sebuah array objek PlaneLayout, satu untuk setiap plane dalam frame. Setiap objek PlaneLayout berisi dua informasi penting:
offset: Offset byte di mana data plane ini dimulai di dalam buffer tujuan.stride: Jumlah byte antara awal satu baris piksel dan awal baris berikutnya untuk plane tersebut.
Konsep Kritis: Stride vs. Width
Ini adalah salah satu sumber kebingungan paling umum bagi pengembang yang baru mengenal pemrograman grafis tingkat rendah. Anda tidak bisa berasumsi bahwa setiap baris data piksel tersusun rapat satu sama lain.
- Width adalah jumlah piksel dalam satu baris gambar.
- Stride (juga disebut pitch atau line step) adalah jumlah byte di memori dari awal satu baris ke awal baris berikutnya.
Seringkali, stride akan lebih besar dari width * bytes_per_pixel. Ini karena memori sering diberi padding agar selaras dengan batas perangkat keras (misalnya, batas 32 atau 64-byte) untuk pemrosesan yang lebih cepat oleh CPU atau GPU. Anda harus selalu menggunakan stride untuk menghitung alamat memori sebuah piksel di baris tertentu.
Mengabaikan stride akan menyebabkan gambar miring atau terdistorsi dan akses data yang salah.
Contoh Praktis 1: Mengakses dan Menampilkan Plane Grayscale
Mari kita mulai dengan contoh yang sederhana namun kuat. Sebagian besar video di web dienkode dalam format YUV seperti I420. Plane 'Y' secara efektif merupakan representasi grayscale lengkap dari gambar. Kita dapat mengekstrak hanya plane ini dan me-render-nya ke canvas.
async function displayGrayscale(videoFrame) {
// Kita asumsikan videoFrame dalam format YUV seperti 'I420' atau 'NV12'.
if (!videoFrame.format.startsWith('I4')) {
console.error('Contoh ini memerlukan format planar YUV 4:2:0.');
videoFrame.close();
return;
}
const yPlaneInfo = videoFrame.layout[0]; // Plane Y selalu yang pertama.
// Buat buffer untuk menampung hanya data plane Y.
const yPlaneData = new Uint8Array(yPlaneInfo.stride * videoFrame.codedHeight);
// Salin plane Y ke dalam buffer kita.
await videoFrame.copyTo(yPlaneData, {
rect: { x: 0, y: 0, width: videoFrame.codedWidth, height: videoFrame.codedHeight },
layout: [yPlaneInfo]
});
// Sekarang, yPlaneData berisi piksel grayscale mentah.
// Kita perlu me-render-nya. Kita akan membuat buffer RGBA untuk canvas.
const canvas = document.getElementById('my-canvas');
canvas.width = videoFrame.displayWidth;
canvas.height = videoFrame.displayHeight;
const ctx = canvas.getContext('2d');
const imageData = ctx.createImageData(canvas.width, canvas.height);
// Iterasi piksel canvas dan isi dari data plane Y.
for (let y = 0; y < videoFrame.displayHeight; y++) {
for (let x = 0; x < videoFrame.displayWidth; x++) {
// Penting: Gunakan stride untuk menemukan indeks sumber yang benar!
const yIndex = y * yPlaneInfo.stride + x;
const luma = yPlaneData[yIndex];
// Hitung indeks tujuan di dalam buffer RGBA ImageData.
const rgbaIndex = (y * canvas.width + x) * 4;
imageData.data[rgbaIndex] = luma; // Merah
imageData.data[rgbaIndex + 1] = luma; // Hijau
imageData.data[rgbaIndex + 2] = luma; // Biru
imageData.data[rgbaIndex + 3] = 255; // Alfa
}
}
ctx.putImageData(imageData, 0, 0);
// KRITIS: Selalu tutup VideoFrame untuk melepaskan memorinya.
videoFrame.close();
}
Contoh ini menyoroti beberapa langkah kunci: mengidentifikasi tata letak plane yang benar, mengalokasikan buffer tujuan, menggunakan copyTo untuk mengekstrak data, dan melakukan iterasi data dengan benar menggunakan stride untuk membuat gambar baru.
Contoh Praktis 2: Manipulasi Langsung (Filter Sepia)
Sekarang mari kita lakukan manipulasi data secara langsung. Filter sepia adalah efek klasik yang mudah diimplementasikan. Untuk contoh ini, lebih mudah bekerja dengan frame RGBA, yang mungkin Anda dapatkan dari canvas atau konteks WebGL.
async function applySepiaFilter(videoFrame) {
// Contoh ini mengasumsikan frame input adalah 'RGBA' atau 'BGRA'.
if (videoFrame.format !== 'RGBA' && videoFrame.format !== 'BGRA') {
console.error('Contoh filter sepia memerlukan frame RGBA.');
videoFrame.close();
return null;
}
// Alokasikan buffer untuk menampung data piksel.
const frameDataSize = videoFrame.allocationSize();
const frameData = new Uint8Array(frameDataSize);
await videoFrame.copyTo(frameData);
const layout = videoFrame.layout[0]; // RGBA adalah satu plane tunggal
// Sekarang, manipulasi data di dalam buffer.
for (let y = 0; y < videoFrame.codedHeight; y++) {
for (let x = 0; x < videoFrame.codedWidth; x++) {
const pixelIndex = y * layout.stride + x * 4; // 4 byte per piksel (R,G,B,A)
const r = frameData[pixelIndex];
const g = frameData[pixelIndex + 1];
const b = frameData[pixelIndex + 2];
const tr = 0.393 * r + 0.769 * g + 0.189 * b;
const tg = 0.349 * r + 0.686 * g + 0.168 * b;
const tb = 0.272 * r + 0.534 * g + 0.131 * b;
frameData[pixelIndex] = Math.min(255, tr);
frameData[pixelIndex + 1] = Math.min(255, tg);
frameData[pixelIndex + 2] = Math.min(255, tb);
// Alfa (frameData[pixelIndex + 3]) tidak diubah.
}
}
// Buat VideoFrame *baru* dengan data yang telah dimodifikasi.
const newFrame = new VideoFrame(frameData, {
format: videoFrame.format,
codedWidth: videoFrame.codedWidth,
codedHeight: videoFrame.codedHeight,
timestamp: videoFrame.timestamp,
duration: videoFrame.duration
});
// Jangan lupa untuk menutup frame asli!
videoFrame.close();
return newFrame;
}
Ini menunjukkan siklus baca-modifikasi-tulis yang lengkap: salin data keluar, lakukan loop melaluinya menggunakan stride, terapkan transformasi matematis pada setiap piksel, dan buat VideoFrame baru dengan data yang dihasilkan. Frame baru ini kemudian dapat di-render ke canvas, dikirim ke VideoEncoder, atau diteruskan ke langkah pemrosesan lainnya.
Kinerja Itu Penting: JavaScript vs. WebAssembly (WASM)
Melakukan iterasi jutaan piksel untuk setiap frame (frame 1080p memiliki lebih dari 2 juta piksel, atau 8 juta titik data dalam RGBA) di JavaScript bisa menjadi lambat. Meskipun mesin JS modern sangat cepat, untuk pemrosesan real-time video resolusi tinggi (HD, 4K), pendekatan ini dapat dengan mudah membebani thread utama, yang menyebabkan pengalaman pengguna yang tersendat-sendat.
Di sinilah WebAssembly (WASM) menjadi alat yang penting. WASM memungkinkan Anda menjalankan kode yang ditulis dalam bahasa seperti C++, Rust, atau Go dengan kecepatan mendekati native di dalam browser. Alur kerja untuk pemrosesan video menjadi:
- Di JavaScript: Gunakan
videoFrame.copyTo()untuk mendapatkan data piksel mentah ke dalamArrayBuffer. - Kirim ke WASM: Kirim referensi ke buffer ini ke modul WASM Anda yang telah dikompilasi. Ini adalah operasi yang sangat cepat karena tidak melibatkan penyalinan data.
- Di WASM (C++/Rust): Jalankan algoritma pemrosesan gambar Anda yang sangat dioptimalkan langsung pada buffer memori. Ini berkali-kali lebih cepat daripada loop JavaScript.
- Kembali ke JavaScript: Setelah WASM selesai, kontrol kembali ke JavaScript. Anda kemudian dapat menggunakan buffer yang dimodifikasi untuk membuat
VideoFramebaru.
Untuk aplikasi manipulasi video real-time yang serius—seperti latar belakang virtual, deteksi objek, atau filter kompleks—memanfaatkan WebAssembly bukan hanya pilihan; itu adalah suatu keharusan.
Menangani Format Piksel yang Berbeda (misalnya, I420, NV12)
Meskipun RGBA sederhana, Anda akan paling sering menerima frame dalam format YUV planar dari VideoDecoder. Mari kita lihat cara menangani format yang sepenuhnya planar seperti I420.
Sebuah VideoFrame dalam format I420 akan memiliki tiga deskriptor tata letak dalam array layout-nya:
layout[0]: Plane Y (luma). Dimensi adalahcodedWidthxcodedHeight.layout[1]: Plane U (chroma). Dimensi adalahcodedWidth/2xcodedHeight/2.layout[2]: Plane V (chroma). Dimensi adalahcodedWidth/2xcodedHeight/2.
Berikut cara Anda akan menyalin ketiga plane ke dalam satu buffer:
async function extractI420Planes(videoFrame) {
const totalSize = videoFrame.allocationSize({ format: 'I420' });
const allPlanesData = new Uint8Array(totalSize);
const layouts = await videoFrame.copyTo(allPlanesData);
// layouts adalah sebuah array berisi 3 objek PlaneLayout
console.log('Tata Letak Plane Y:', layouts[0]); // { offset: 0, stride: ... }
console.log('Tata Letak Plane U:', layouts[1]); // { offset: ..., stride: ... }
console.log('Tata Letak Plane V:', layouts[2]); // { offset: ..., stride: ... }
// Anda sekarang dapat mengakses setiap plane di dalam buffer `allPlanesData`
// menggunakan offset dan stride spesifiknya.
const yPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[0].offset,
layouts[0].stride * videoFrame.codedHeight
);
// Perhatikan dimensi chroma dibagi dua!
const uPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[1].offset,
layouts[1].stride * (videoFrame.codedHeight / 2)
);
const vPlaneView = new Uint8Array(
allPlanesData.buffer,
layouts[2].offset,
layouts[2].stride * (videoFrame.codedHeight / 2)
);
console.log('Ukuran plane Y yang diakses:', yPlaneView.byteLength);
console.log('Ukuran plane U yang diakses:', uPlaneView.byteLength);
videoFrame.close();
}
Format umum lainnya adalah NV12, yang bersifat semi-planar. Format ini memiliki dua plane: satu untuk Y, dan plane kedua di mana nilai U dan V diselingi (misalnya, [U1, V1, U2, V2, ...]). API WebCodecs menangani ini secara transparan; sebuah VideoFrame dalam format NV12 hanya akan memiliki dua tata letak dalam array layout-nya.
Tantangan dan Praktik Terbaik
Bekerja pada tingkat rendah ini sangat kuat, tetapi disertai dengan tanggung jawab.
Manajemen Memori adalah yang Utama
Sebuah VideoFrame menampung sejumlah besar memori, yang sering dikelola di luar heap pengumpul sampah JavaScript. Jika Anda tidak secara eksplisit melepaskan memori ini, Anda akan menyebabkan kebocoran memori yang dapat membuat tab browser mogok.
Selalu, selalu panggil videoFrame.close() ketika Anda selesai dengan sebuah frame.
Sifat Asinkron
Semua akses data bersifat asinkron. Arsitektur aplikasi Anda harus menangani alur Promise dan async/await dengan benar untuk menghindari kondisi balapan (race conditions) dan memastikan alur pemrosesan yang lancar.
Kompatibilitas Browser
WebCodecs adalah API modern. Meskipun didukung di semua browser utama, selalu periksa ketersediaannya dan waspadai detail implementasi atau batasan spesifik dari vendor. Gunakan deteksi fitur sebelum mencoba menggunakan API.
Kesimpulan: Batas Baru untuk Video Web
Kemampuan untuk mengakses dan memanipulasi data plane mentah dari sebuah VideoFrame secara langsung melalui API WebCodecs adalah pergeseran paradigma untuk aplikasi media berbasis web. Ini menghilangkan kotak hitam dari elemen <video> dan memberi pengembang kontrol granular yang sebelumnya hanya diperuntukkan bagi aplikasi native.
Dengan memahami dasar-dasar tata letak memori video—plane, stride, dan format warna—dan dengan memanfaatkan kekuatan WebAssembly untuk operasi yang kritis terhadap kinerja, Anda sekarang dapat membangun alat pemrosesan video yang sangat canggih langsung di browser. Dari gradasi warna real-time dan efek visual kustom hingga machine learning sisi klien dan analisis video, kemungkinannya sangat luas. Era video berkinerja tinggi dan tingkat rendah di web telah benar-benar dimulai.